﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InvokeMethod.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Windows.Input
{
    using System;
    using System.Collections.Specialized;
    using System.ComponentModel;
    using System.Linq;
    using System.Windows;
    using System.Windows.Interactivity;
    using System.Windows.Markup;
    

#if SILVERLIGHT
    using CommandManager = Commands.CommandManager;
    using Trace = Diagnostics.Trace;
#else
    using CommandManager = System.Windows.Input.CommandManager;
    using Trace = System.Diagnostics.Trace;
#endif

    /// <summary>
    /// You can use the <see cref="InvokeMethod"/> action to call a method that is defined for a specified object.
    /// </summary>
    [ContentProperty("Arguments")]
    public class InvokeMethod : TriggerAction<FrameworkElement>
    {
        #region Fields

        private object _target;
        private readonly RoutedPropertyChangedEventHandler<object> _dataContextChangedWeakHandler;
        private readonly EventHandler _commandRequerySuggestedWeakHandler;
        private ScopePropertyObserver _targetPropertyClient;
        private InvokeMethodBinding _methodBinding;
        private readonly EventHandler _onArgumentRequerySuggested;

        #region Dependency Properties 

        /// <summary>
        /// Identifies the <see cref="MethodName"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty MethodNameProperty = DependencyProperty.Register("MethodName", 
            typeof(string), typeof(InvokeMethod), new PropertyMetadata(MethodNamePropertyChanged));

        /// <summary>
        /// identifies the <see cref="Arguments"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty ArgumentsProperty = DependencyProperty.Register("Arguments", 
            typeof(ArgumentCollection), typeof(InvokeMethod), new PropertyMetadata(ArgumentsPropertyChanged));

        /// <summary>
        /// identifies the <see cref="FeedbackPresenter"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty FeedbackPresenterProperty = DependencyProperty.Register("FeedbackPresenter", 
            typeof(IFeedbackPresenter), typeof(InvokeMethod), 
            new PropertyMetadata(Input.FeedbackPresenter.Default, FeedbackPresenterPropertyChanged));

        private bool _canExecute;

        #endregion
        
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="InvokeMethod"/> class.
        /// </summary>
        /// <param name="methodName">Name of the method.</param>
        public InvokeMethod(string methodName) : this()
        {
            MethodName = methodName;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="InvokeMethod"/> class.
        /// </summary>
        public InvokeMethod()
        {
            SetValue(ArgumentsProperty, new ArgumentCollection());
            _commandRequerySuggestedWeakHandler = OnCommandRequerySuggested;
            _dataContextChangedWeakHandler = OnDataContextChanged;
            _onArgumentRequerySuggested = OnArgumentRequerySuggested;
        }

        private void OnCommandRequerySuggested(object sender, EventArgs e)
        {
            Update();
        }

        private void Update()
        {
            UpdateCanExecute();
            UpdateFeedback();
        }

        private void UpdateCanExecute()
        {
            CanExecute = MethodBinding != null && MethodBinding.CanExecute(Target, 
                Arguments.Select(x => ((IArgument)x).Evaluate(null)).ToList());
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance can execute.
        /// </summary>
        /// <value><c>true</c> if this instance can execute; otherwise, <c>false</c>.</value>
        public bool CanExecute
        {
            get
            {
                return _canExecute;
            }
            private set
            {
                if (_canExecute != value)
                {
                    _canExecute = value;
                    UpdateFeedback();
                }
            }
        }
        
        private void UpdateFeedback()
        {
            if (FeedbackPresenter != null && AssociatedObject != null)
            {
                FeedbackPresenter.ProvideFeedback(AssociatedObject, this);
            }
        }

        /// <summary>
        /// Gets or sets the method name.
        /// </summary>
        /// <value>The expression.</value>
        public string MethodName
        {
            get { return (string)GetValue(MethodNameProperty); }
            set { SetValue(MethodNameProperty, value); }
        }

        /// <summary>
        /// Gets or sets the feedback presenter.
        /// </summary>
        /// <value>The feedback presenter.</value>
        public IFeedbackPresenter FeedbackPresenter
        {
            get { return (IFeedbackPresenter)GetValue(FeedbackPresenterProperty); }
            set { SetValue(FeedbackPresenterProperty, value); }
        }

        /// <summary>
        /// Gets the arguments.
        /// </summary>
        /// <value>The arguments.</value>
        public ArgumentCollection Arguments
        {
            get
            {
                var arguments = (ArgumentCollection)GetValue(ArgumentsProperty);
                if (arguments == null)
                {
                    arguments = new ArgumentCollection();
                    SetValue(ArgumentsProperty, arguments);
                }

                return arguments;
            }
        }

        /// <summary>
        /// Gets the target.
        /// </summary>
        /// <value>The target.</value>
        protected internal object Target
        {
            get
            {
                return _target;
            }
            private set
            {
                if (_target != value)
                {
                    var oldValue = _target;
                    _target = value;

                    // notify
                    TargetChanged(oldValue, value);
                }
            }
        }

        private void TargetChanged(object oldValue, object newValue)
        {
            var oldTargetType = oldValue != null ? oldValue.GetType() : null;
            var newTargetType = newValue != null ? newValue.GetType() : null;
            if (!Equals(oldTargetType, newTargetType))
            {
                UpdateBinding();
            }
            Update();
        }

        /// <summary>
        /// Called after the action is attached to an AssociatedObject.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            AttachEvents();
            UpdateFeedback();
            _targetPropertyClient = new ScopePropertyObserver(AssociatedObject, Input.Invoke.TargetScopeProperty);
            _targetPropertyClient.ValueChanged += OnInvokeTargetChanged;
            UpdateTarget();
        }

        private void OnInvokeTargetChanged(object sender, EventArgs e)
        {
            UpdateTarget();
        }

        /// <summary>
        /// Called when the action is being detached from its AssociatedObject, but before it has actually occurred.
        /// </summary>
        protected override void OnDetaching()
        {
            base.OnDetaching();
            DetachEvents();
            if (FeedbackPresenter != null)
            {
                FeedbackPresenter.Reset(AssociatedObject, this);
            }

            _targetPropertyClient.ValueChanged -= OnInvokeTargetChanged;
            _targetPropertyClient = null;
        }


        /// <summary>
        /// Invokes the specified parameter.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        protected override void  Invoke(object parameter)
       {
            if (MethodBinding != null)
            {
                MethodBinding.Execute(Target, 
                    Arguments.Select(x => ((IArgument)x).Evaluate(parameter)).ToList());
            }

            // often operations are not available immediately after calling, for example during 
            // asynchronouse calls, etc. so refresh state now
            CommandManager.InvalidateRequerySuggested();
        }

        /// <summary>
        /// Detaches the events.
        /// </summary>
        private void DetachEvents()
        {
            DataContextListener.RemoveHandler(AssociatedObject, this._dataContextChangedWeakHandler);
            CommandManager.RequerySuggested -= _commandRequerySuggestedWeakHandler;
        }
        
        private void AttachEvents()
        {
            DataContextListener.AddHandler(AssociatedObject, this._dataContextChangedWeakHandler);
            CommandManager.RequerySuggested += _commandRequerySuggestedWeakHandler;
        }

        private static void MethodNamePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            ((InvokeMethod)obj).UpdateBinding();
        }

        private static void FeedbackPresenterPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            ((InvokeMethod)obj).OnFeedbackPresenterChanged((IFeedbackPresenter)e.OldValue);
        }

        private static void ArgumentsPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            ((InvokeMethod)obj).OnArgumentsChanged((ArgumentCollection)e.OldValue, (ArgumentCollection)e.NewValue);
        }

        private void OnFeedbackPresenterChanged(IFeedbackPresenter oldValue)
        {
            if (oldValue != null)
            {
                oldValue.Reset(AssociatedObject, this);
            }

            UpdateFeedback();
        }

        private void OnArgumentsChanged(AttachedCollection<ArgumentBase> oldValue, AttachedCollection<ArgumentBase> newValue)
        {
            if (oldValue != newValue)
            {
                if (oldValue != null)
                {
                    DetachArgumentCollection(oldValue);
                }

                if (newValue != null)
                {
                    AttachArgumentCollection(newValue);
                }
            }
        }

        private void AttachArgumentCollection(AttachedCollection<ArgumentBase> newValue)
        {
            newValue.ItemDetached += OnArgumentDetached;
            newValue.ItemAttached += OnArgumentAttached;
            ((IAttachedObject)newValue).Attach(this);
            newValue.CollectionChanged += OnArgumentsCollectionChanged;
        }

        private void DetachArgumentCollection(AttachedCollection<ArgumentBase> oldValue)
        {
            ((IAttachedObject)oldValue).Detach();
            oldValue.ItemDetached -= OnArgumentDetached;
            oldValue.ItemAttached -= OnArgumentAttached;
            oldValue.CollectionChanged -= OnArgumentsCollectionChanged;
        }

        private void OnArgumentAttached(object sender, AttachedItemEventArgs<ArgumentBase> e)
        {
            e.Item.RequerySuggested += _onArgumentRequerySuggested;
        }

        private void OnArgumentDetached(object sender, AttachedItemEventArgs<ArgumentBase> e)
        {
            e.Item.RequerySuggested -= _onArgumentRequerySuggested;
        }

        private void OnArgumentsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Update();
        }

        private void OnDataContextChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            UpdateTarget();
        }

        internal void OnArgumentRequerySuggested(object sender, EventArgs eventArgs)
        {
            if (MethodBinding != null)
            {
                // TODO: In the case of argument changes we don't need to invalidate everything
                OnCommandRequerySuggested(null, EventArgs.Empty);
            }
        }

        private void UpdateBinding()
        {
            if (string.IsNullOrEmpty(MethodName) || Target == null)
            {
                MethodBinding = null;
                return;
            }

            var binder = new InvokeMethodBinder();
            
            MethodBinding = binder.Bind(Target.GetType(), MethodName);
            if (MethodBinding == null)
            {
                TraceBindingError();
            }
        }

        private void TraceBindingError()
        {
            var targetDescription = "DataContext";
            if (!this.IsUsingDataContextAsTarget)
            {
                targetDescription = "Invoke.Target";
                var source = _targetPropertyClient.Source;
                if (source != null)
                {
                    var sourceType = source.GetType().FullName;
                    var sourceName = string.Empty;
                    var sourceElement = source as FrameworkElement;
                    if (sourceElement != null)
                    {
                        sourceName = sourceElement.Name ?? string.Empty;
                    }
                    targetDescription += string.Format(" {{ SourceType = {0}, SourceName = '{1}' }}", sourceType, sourceName);
                }
            }
            Trace.WriteLine(string.Format(ExceptionMessages.MethodInvokeActionBindingError, 
                this.MethodName, this.Target.GetType().FullName, targetDescription));
        }

        private bool IsUsingDataContextAsTarget
        {
            get { return Equals(_targetPropertyClient.Value, Input.Invoke.TargetScopeProperty.DefaultMetadata.UnsetValue); }
        }

        [Browsable(false)]
        public InvokeMethodBinding MethodBinding
        {
            get
            {
                return _methodBinding;
            }
            set
            {
                if (_methodBinding != value)
                {
                    _methodBinding = value;
                    Update();
                }
            }
        }

        private void UpdateTarget()
        {
            object target = null;
            if (AssociatedObject != null && _targetPropertyClient != null)
            {
                target = IsUsingDataContextAsTarget ? AssociatedObject.DataContext : _targetPropertyClient.Value;
            }
            
            Target = target;
        }
    }
}